package crawler

import (
	"fmt"
	"net/url"
	"regexp"
	"strings"
	"sync/atomic"
	"time"

	colly "github.com/gocolly/colly/v2"
	"github.com/gocolly/colly/v2/extensions"
	log "github.com/sirupsen/logrus"
	"github.com/twang2218/herbs/common"
)

type PrescriptionCrawler struct {
	PageVisited int32
	PageTotal   int32

	cItem     *colly.Collector
	cIndex    *colly.Collector
	listeners []func(common.Prescription)
}

const LINK_PRESCRIPTION string = "http://zhongyaofangji.com/"

func NewPrescriptionCrawler() *PrescriptionCrawler {
	pc := PrescriptionCrawler{}
	//	创建基础爬虫
	u, err := url.Parse(LINK_PRESCRIPTION)
	if err != nil {
		log.Fatal(err)
	}

	pc.cItem = colly.NewCollector(
		colly.AllowedDomains(u.Hostname()),
		colly.DetectCharset(),
		colly.URLFilters(
			regexp.MustCompile(fmt.Sprintf(`^%s$`, LINK_PRESCRIPTION)),
			regexp.MustCompile(fmt.Sprintf(`^%s\w/$`, LINK_PRESCRIPTION)),
			regexp.MustCompile(fmt.Sprintf(`^%s\w/.*.html$`, LINK_PRESCRIPTION)),
		),
		colly.CacheDir(CRAWLER_CACHE_DIR),
		colly.Async(true),
	)
	pc.cItem.Limit(&colly.LimitRule{DomainGlob: "*", Parallelism: CRAWLER_PARALLELISM})
	pc.cItem.SetRequestTimeout(CRAWLER_REQUEST_TIMEOUT)
	extensions.RandomUserAgent(pc.cItem)
	pc.cItem.OnRequest(func(r *colly.Request) {
		r.ResponseCharacterEncoding = "gb2312"
		atomic.AddInt32(&pc.PageTotal, 1)
	})
	pc.cItem.OnResponse(func(r *colly.Response) {
		atomic.AddInt32(&pc.PageVisited, 1)
	})
	pc.cItem.OnError(func(resp *colly.Response, err error) {
		log.Warnf("PrescriptionCrawler.OnError(): [Item] (%s) => '%s'", resp.Request.URL, err)
		//	重试。在另外线程等待一段时间，以不阻碍当前线程（爬虫）运行。
		go func(link string) {
			time.Sleep(CRAWLER_RETRY_TIMEOUT)
			pc.cItem.Visit(link)
		}(resp.Request.URL.String())
	})
	//	添加解析方剂函数
	pc.cItem.OnHTML(".post", pc.parseItem)

	//	创建索引爬虫
	pc.cIndex = pc.cItem.Clone()
	extensions.RandomUserAgent(pc.cIndex)
	pc.cIndex.OnRequest(func(r *colly.Request) {
		r.ResponseCharacterEncoding = "gb2312"
		log.Debugf("PrescriptionCrawler => %s", r.URL)
	})
	pc.cIndex.OnError(func(resp *colly.Response, err error) {
		log.Warnf("PrescriptionIndexCrawler.OnError(): [Index] (%s) => '%s'", resp.Request.URL, err)
		//	重试。在另外线程等待一段时间，以不阻碍当前线程（爬虫）运行。
		go func(link string) {
			time.Sleep(CRAWLER_RETRY_TIMEOUT)
			pc.cIndex.Visit(link)
		}(resp.Request.URL.String())
	})
	//	添加索引解析函数
	pc.cIndex.OnHTML(".logo-a-z", pc.parseIndexPage)
	pc.cIndex.OnHTML(".uzyc", pc.parseIndex)

	pc.listeners = []func(common.Prescription){}

	return &pc
}

func (c *PrescriptionCrawler) Collect() {
	//	开始抓取
	c.cIndex.Visit(LINK_PRESCRIPTION)
	//	等待结束
	c.cIndex.Wait()
	c.cItem.Wait()
}

func (c *PrescriptionCrawler) parseItem(e *colly.HTMLElement) {
	p := common.Prescription{
		Name:         strings.TrimSpace(e.ChildText(".post-title")),
		Link:         e.Request.URL.String(),
		Descriptions: []common.PrescriptionDescription{},
	}

	// texts := e.ChildTexts(".spider > h2, .spider > p, .spider > .yfpzz")
	origin := ""
	desc := []string{}
	e.ForEach(".spider > h2, .spider > p, .spider > .yfpzz", func(i int, h *colly.HTMLElement) {
		// log.Tracef("Name: %s, Text: %s", h.Name, h.Text)
		if h.Name == "h2" {
			// log.Tracef("<h2> old origin: %s => new origin: %s", origin, h.Text)
			if len(origin) > 0 {
				// log.Tracef("len(origin) > 0")
				// 创建 Prescription
				pd := parsePrescriptionDescription(p.Name, origin, strings.Join(desc, "\n"))
				p.Descriptions = append(p.Descriptions, pd)
				origin = ""
			}

			origin = h.Text
			desc = []string{}
		} else {
			desc = append(desc, h.Text)
		}
		// log.Tracef("origin: %s, [%v]", origin, desc)
	})

	if len(origin) > 0 {
		pd := parsePrescriptionDescription(p.Name, origin, strings.Join(desc, "\n"))
		p.Descriptions = append(p.Descriptions, pd)
	}

	//	打印解析结果（测试）
	// b, _ := json.MarshalIndent(p, "", "  ")
	// log.Trace(string(b))

	log.Debugf("「%s」:  (%s)", p.Name, p.Link)
	//	打印方剂出处
	var books []string
	for _, e := range p.Descriptions {
		books = append(books, fmt.Sprintf("《%s》", e.Book))
	}
	log.Tracef(" >  【%d 个出处】 %s", len(books), strings.Join(books, "、"))

	//	通知 OnPrescriptionListeners
	c.notifyAllOnPrescriptionListeners(p)
}

var reBookMark = regexp.MustCompile(`[〈《》}（）\)：，]`)

func parsePrescriptionDescriptionBookmark(pd *common.PrescriptionDescription, name, bookmark string) {
	//	PrescriptionDescription{Book, Volume, Name}

	//	解析出处
	//	  1. 以 "：" 分割`书籍` `卷号` 和`方剂名`
	//	  2. 解析书卷
	//		2.1 如果包含 "《"，则以 "》})" 分割 `书籍` 和 `卷号`；然后，去除`书籍`中的"《"
	//		2.2 否则，如果结尾是 "方"，如果长度超过4个汉字，则警告长度异常；整体作为`书籍`

	bvn := reBookMark.Split(bookmark, -1)

	if len(bvn) == 2 {
		//	周明道方：柏子仁汤: 	 []string{"周明道方", "柏子仁汤"}
		pd.Book = bvn[0]
		pd.Name = bvn[1]
		if strings.HasSuffix(bvn[0], "方") {
			//	施今墨方：白果汤
		} else {
			//	异常
			log.Warnf("「%s」 解析【书籍】、【方剂名】出现异常。'%s'", name, bookmark)
		}
	} else if len(bvn) > 3 {
		//	【4】
		//	《景岳全书）卷五十一：猎虫丸: 	=>	 []string{"", "景岳全书", "卷五十一", "猎虫丸"}
		//	〈圣济总录》卷一六○：地黄饮' => []string{"", "圣济总录", "卷一六○", "地黄饮"}
		//	【6】
		//	《千金翼》卷二十一引《耆婆方》：阿魏雷丸散	=>	[]string{"", "千金翼", "卷二十一引", "耆婆方", "", "阿魏雷丸散"}
		//	【7】
		//	方出《圣惠》卷二十二，名见《圣济总录》卷十：阿魏散'		=>	[]string{"方出", "圣惠", "卷二十二", "名见", "圣济总录", "卷十", "阿魏散"}
		pd.Book = bvn[1]
		pd.Volume = strings.TrimRight(bvn[2], "引")
		size := len(bvn) - 1
		pd.Name = bvn[size]

		switch bvn[0] {
		case "", "方出": // 正常解析
		default:
			if strings.HasPrefix(bvn[0], "明") || strings.HasPrefix(bvn[0], "唐") || strings.HasSuffix(bvn[0], "氏") {
				//	'明·方贤着《奇效良方》：白矾散'  []string{"明·方贤着", "奇效良方", "", "白矾散"}
				//	'年氏《集验良方》卷四：利膈化痰丸' => []string{"年氏", "集验良方", "卷四", "利膈化痰丸"}
			} else {
				if bvn[1] == "见" || bvn[1] == "" {
					//	'方出贾同知引康少尹方（见《宣明论》卷九），名见《本草纲目》卷二十三：百劳散' => []string{"方出贾同知引康少尹方", "见", "宣明论", "卷九", "", "名见", "本草纲目", "卷二十三", "百劳散"}
					//	'张民庆等：《肿瘤良方大全》：加减八珍汤' => []string{"张民庆等", "", "肿瘤良方大全", "", "加减八珍汤"}
					pd.Book = bvn[2]
					pd.Volume = strings.TrimRight(bvn[3], "引")
					size := len(bvn) - 1
					pd.Name = bvn[size]
				} else {
					if pd.Book == "" || pd.Name == "" {
						// 异常解析
						log.Warnf("「%s」 解析【书籍】、【卷号】、【方剂名】出现异常。'%s' => %#v %#v", name, bookmark, bvn, pd)
					}
				}
			}
		}
	} else {
		log.Warnf("「%s」 无法解析出处：'%s' => %#v", name, bookmark, bvn)
	}
}

func parsePrescriptionDescriptionDetails(pd *common.PrescriptionDescription, name, desc string) {
	//	common.PrescriptionDescription{Recipe, Indication, Alias, Origin, Dosage, Process, Notes, Extra}

	//	解析方剂描述
	//	如：
	//	--------
	// 【处方】半夏1钱半，茯苓1钱半，陈皮1钱半，枳实1钱2分，桔梗1钱，瓜蒌仁（去油）1钱，黄连1钱，香附1钱，甘草3分。
	// 【功能主治】气结痰壅膈噎，饮食不下。
	// 方剂类型：解表剂、清热剂、泻下剂、祛风湿剂、祛湿利水剂、温里剂、理气剂、消导剂、驱虫剂、止血剂、活血剂、化痰止咳平喘剂、安神剂、熄风剂、开窍剂、补益剂、固涩剂。
	// 方剂剂型：汤剂、丸剂、散剂、膏剂、丹剂、酒剂、药酒、冲剂、口服液剂、胶囊剂、片剂
	// 【用法用量】上用水2钟，煎8分，加竹沥半盏，姜汁2-3匙，食前服。
	// 【摘录】《济阳纲目》卷三十六
	//	--------
	lines := strings.Split(desc, "【")
	if len(lines) > 1 {
		for _, line := range lines {
			if len(line) == 0 {
				//	跳过空行
				continue
			}

			//	取得键、值
			ws := strings.Split(line, "】")

			var key string
			var value string

			switch len(ws) {
			case 2:
				key = strings.TrimSpace(ws[0])
				value = strings.TrimSpace(ws[1])
			case 3:
				key = strings.TrimSpace(ws[0])
				value = strings.TrimSpace(ws[2])
			default:
				log.Warnf("「%s」 无法拆分键值: '%s' => %#v", name, line, ws)
				continue
			}

			//	解析键值
			switch key {
			case "处方":
				pd.Recipe = value
			case "功能主治":
				pd.Indication = value
			case "别名":
				pd.Alias = value
			case "摘录":
				pd.Origin = value
			case "用法用量":
				pd.Dosage = value
			case "制法", "炮制":
				pd.Process = value
			case "注意", "备注":
				pd.Notes = value
			default:
				if pd.Extra == nil {
					pd.Extra = map[string]string{}
				}
				pd.Extra[key] = value
			}
		}

		if len(pd.Recipe) == 0 {
			//	若"处方"为空，则从 "来源" 获取"处方"
			if v, ok := pd.Extra["来源"]; ok {
				pd.Recipe = v
				delete(pd.Extra, "来源")
			} else {
				if len(pd.Extra) > 0 {
					log.Warnf("「%s」 方剂解释中没有处方，但存在额外解释。'%v'", name, pd.Extra)
				}
			}
		}
	} else {
		log.Warnf("「%s」 无法解析方剂描述项目：'%s'", name, desc)
	}
}

func parsePrescriptionDescription(name, origin, desc string) common.PrescriptionDescription {
	pd := common.PrescriptionDescription{Raw: desc}

	//	解析出处
	parsePrescriptionDescriptionBookmark(&pd, name, origin)
	//	解析方剂描述
	parsePrescriptionDescriptionDetails(&pd, name, desc)

	return pd
}

func (c *PrescriptionCrawler) parseIndexPage(e *colly.HTMLElement) {
	// log.Debug(".logo-a-z")
	e.ForEach("a", func(_ int, e1 *colly.HTMLElement) {
		link := e1.Request.AbsoluteURL(strings.TrimSpace(e1.Attr("href")))
		// log.Debugf("Link: %s", link)
		if len(link) > 1 {
			// TODO: 取消提前终止，测试用
			// if len(presNames) >= 1000 {
			// 	// log.Debug("<END 1>")
			// 	return
			// }
			c.cIndex.Visit(link)
			// log.Debug()
		}
	})
}

func (c *PrescriptionCrawler) parseIndex(e *colly.HTMLElement) {
	// log.Debug(".uzyc")
	e.ForEach("a", func(_ int, e1 *colly.HTMLElement) {
		link := strings.TrimSpace(e1.Attr("href"))

		if strings.Contains(link, "/yaofang/") {
			// '/yaofang/xxx.html' 的格式不标准，暂时不考虑
			return
		}

		// TODO: 取消提前终止，测试用
		// if len(presNames) >= 1000 {
		// 	// log.Debug("<END 2>")
		// 	return
		// }

		// log.Infof("[%s]: %s", name, link)

		// log.Infof("crIndex.OnHTML(): before cr.Visit(link)")
		c.cItem.Visit(link)
		// log.Infof("crIndex.OnHTML(): after cr.Visit(link)")
	})
}

//	Listener functions

func (c *PrescriptionCrawler) AddOnPrescriptionListener(f func(common.Prescription)) {
	if f == nil {
		log.Warn("PrescriptionCrawler.AddOnPrescriptionListener(): couldn't add 'nil' as listener.")
		return
	}

	c.listeners = append(c.listeners, f)
}

func (c *PrescriptionCrawler) ClearOnPrescriptionListener() {
	c.listeners = []func(common.Prescription){}
}

func (c *PrescriptionCrawler) notifyAllOnPrescriptionListeners(h common.Prescription) {
	for _, listener := range c.listeners {
		listener(h)
	}
}
